-- ============================================================================
-- ModbusRTU_Slave.lua
-- author:          read
-- version:         1.0.0
-- Created on:      2025-06-16
-- Last modified:   2025-09-10
-- 描述:
--   本库实现了完整的 Modbus RTU 从站功能，支持多种功能码操作，提供简洁易用的 API 接口，
--   适用于工业自动化场景中的设备从站实现。库包含完整的异常处理机制、数据存储区管理，
--   以及符合 Modbus RTU 规范的通信协议实现。
-- ============================================================================
-- 目前只实现以下功能码处理:
--  0x03 (0x03) - 读取保持寄存器
--  0x06 (0x06) - 写入单个保持寄存器
--  0x10 (0x10) - 写入多个保持寄存器
--  0x01 (0x01) - 读取输入线圈状态
--  0x05 (0x05) - 写入单个输入线圈状态
--  0x0f (0x0F) - 写入多个输入线圈状态
-- 库中未实现的功能码
-- 1、0x02 (0x02) - 读取输入寄存器
-- 2、0x04 (0x04) - 读取线圈状态
-- 创建从站实例
local ModbusRTU_Slave = {}
ModbusRTU_Slave.__index = ModbusRTU_Slave

--- === 异常码定义 ===
-- 定义 Modbus 协议中常用的异常码及其含义（采用 PascalCase 风格，与 FunctionCode 一致）
ModbusRTU_Slave.ExceptionCode = {
    IllegalFunction = 0x01, -- 不支持或不允许此功能码
    IllegalDataAddress = 0x02, -- 数据地址无效或超出范围
    IllegalDataValue = 0x03, -- 数据值无效或不可接受
    ServerDeviceFailure = 0x04, -- 从站设备发生不可恢复错误
    Acknowledge = 0x05, -- 请求已被接收，但需等待处理完成
    ServerDeviceBusy = 0x06, -- 从站设备忙，无法处理请求
    NegativeAcknowledge = 0x07, -- 请求被拒绝处理
    MemoryParityError = 0x08, -- 内存奇偶校验失败
    GatewayPathUnavailable = 0x0A, -- 网关路径不可达或不支持
    GatewayTargetDeviceFailed = 0x0B -- 网关连接的目标设备无响应
}

--- === 功能码定义 ===
-- 定义 Modbus 协议中常用的功能码及其含义
ModbusRTU_Slave.FunctionCode = {
    ReadQ = 0x01, -- 读取线圈状态（单个线圈的 ON/OFF 状态）（地址从1开始计数）
    ReadI = 0x02, -- 读取离散输入状态（只读输入点的状态）
    ReadVW = 0x03, -- 读取保持寄存器（可读写的数据寄存器）
    ReadAIW = 0x04, -- 读取输入寄存器（只读的输入数据寄存器）
    WriteQ = 0x05, -- 写入单个线圈（设置单个线圈的 ON/OFF 状态）（地址从1开始计数）
    WriteVW = 0x06, -- 写入单个寄存器（设置单个保持寄存器的值）
    WriteMQ = 0x0F, -- 写入多个线圈（批量设置线圈状态）（地址从1开始计数）
    WriteMVW = 0x10 -- 写入多个寄存器（批量设置保持寄存器的值）
}

-- 在定义后添加局部引用
local ExceptionCode = ModbusRTU_Slave.ExceptionCode
local FunctionCode = ModbusRTU_Slave.FunctionCode

--- === 默认从站配置 ===
local DEFAULT_CONFIG = {
    slave_addr = 0x01, -- 从站 ID，默认为 0x01
    uartid = 1, -- 串口ID
    baudrate = 9600, -- 波特率
    databits = 8, -- 数据位
    stopbits = 1, -- 停止位
    parity = uart.None, -- 校验位
    endianness = uart.LSB, -- 字节序
    buffer_size = 1024 -- 缓冲区大小
    -- gpio_485 = 25, -- 485转向GPIO
    -- rx_level = 0, -- RX电平
    -- tx_delay = 10000 -- TX→RX延迟 (μs)
}

--- === 创建新的从站实例 ===
-- @param config 配置参数
-- @return 从站实例
function ModbusRTU_Slave:new(config)
    local self = setmetatable({}, ModbusRTU_Slave)
    self:init(config or {})
    return self
end

--- === 初始化从站 ===
function ModbusRTU_Slave:init(config)
    -- 合并默认配置
    self.config = {}
    for key, value in pairs(DEFAULT_CONFIG) do
        self.config[key] = config[key] or value
    end

    -- 初始化串口
    uart.setup(self.config.uartid, self.config.baudrate, self.config.databits, self.config.stopbits, self.config.parity,
        self.config.endianness, self.config.buffer_size, self.config.gpio_485, self.config.rx_level,
        self.config.tx_delay)

    log.info("Modbus RTU Slave 初始化配置为:", json.encode(config))

    -- 初始化数据区计数（使用默认值0如果未配置）
    self.coils_count = config.coils_count or config.coil_count or 0 -- 修复：优先使用 coils_count，然后是 coil_count
    self.inputs_count = config.inputs_count or 0
    self.holding_registers_count = config.holding_registers_count or 0
    self.input_registers_count = config.input_registers_count or 0

    -- 独立初始化每个数据区
    self.coils = {}
    for i = 0, self.coils_count - 1 do
        self.coils[i] = 0 -- 线圈状态（bit位）
    end

    self.inputs = {}
    for i = 0, self.inputs_count - 1 do
        self.inputs[i] = 0 -- 离散输入状态
    end

    self.holding_registers = {}
    for i = 0, self.holding_registers_count - 1 do
        self.holding_registers[i] = 0 -- 保持寄存器（16 位整数，可读写）
    end

    self.input_registers = {}
    for i = 0, self.input_registers_count - 1 do
        self.input_registers[i] = 0 -- 输入寄存器（16 位整数，只读）
    end

    log.info("Modbus RTU Slave 初始化完成：地址:", self.config.slave_addr)
    log.info("线圈数量：", self.coils_count)
    log.info("离散输入数量：", self.inputs_count)
    log.info("保持寄存器数量：", self.holding_registers_count)
    log.info("输入寄存器数量：", self.input_registers_count)

    -- 计算3.5个字符时间
    -- 1个字符时间 = 11位/波特率（1位起始位，8位数据位，1位停止位，1位奇偶位）
    -- local char_time = 11 / self.config.baudrate * 1000
    -- self.config.t3_5 = char_time * 3.5
    -- log.info("3.5个字符时间:",self.config.t3_5,"ms")

    -- === 初始化接收状态 ===
    self.rx_buffer = "" -- 接收数据缓冲区
    self.last_rx_time = 0 -- 最后接收时间戳（毫秒）

end

--- === 关闭函数 ===
function ModbusRTU_Slave:close()
    uart.close(self.config.uartid)
    log.info("Modbus RTU Slave 已关闭")
end

--- 读取保持寄存器的值
-- @param address 要读取的寄存器地址（从0 开始计数）
-- @return number 成功时返回寄存器的值，失败返回nil
function ModbusRTU_Slave:read_registers(address)
    if address >= 0 and address < self.holding_registers_count then
        return self.holding_registers[address]
    else
        log.warn("读取保持寄存器的值时，地址越界", "地址=", address, "最大允许地址=",
            self.holding_registers_count - 1)
        return nil
    end
end

--- 写入保持寄存器的值
-- @param address 要写入的寄存器地址（从0 开始计数）
-- @param value 要写入的值
-- @return boolean 成功时返回寄存器的值，失败返回nil
function ModbusRTU_Slave:write_registers(address, value)
    if address >= 0 and address < self.holding_registers_count then
        self.holding_registers[address] = value
        return true
    else
        log.warn("写入保持寄存器的值时，地址越界", "地址=", address, "最大允许地址=",
            self.holding_registers_count - 1)
        return nil
    end
end

--- CRC16 计算
-- @param data 待计算CRC的数据
-- @return number 返回计算出的CRC值
function ModbusRTU_Slave:crc16(data)
    local crc = 0xFFFF
    for i = 1, #data do
        crc = crc ~ data:byte(i)
        for _ = 1, 8 do
            local j = crc & 1
            crc = crc >> 1
            if j ~= 0 then
                crc = crc ~ 0xA001 -- 0xA001 是 CRC16 的生成多项式
            end
        end
    end
    return crc
end

--- 生成异常响应
-- @param fc 功能码
-- @param exception_code 异常码（ExceptionCode 中的值）
-- @return string 异常响应帧（包含CRC）
function ModbusRTU_Slave:generate_exception(fc, exception_code)
    local header = string.pack(">BBB", self.config.slave_addr, fc | 0x80, exception_code)
    local crc = ModbusRTU_Slave:crc16(header)
    return header .. string.pack("<I2", crc)
end

--- CRC 校验函数
-- @param data 接收到的 Modbus RTU 帧数据
-- @return boolean 校验通过返回 true，否则返回 false
function ModbusRTU_Slave:validate_crc(data)
    if #data < 4 then
        log.warn("收到的数据太短，无法解析")
        return false
    end
    local payload = data:sub(1, #data - 2)
    local received_crc = string.sub(data, -2)
    local expected_crc = ModbusRTU_Slave:crc16(payload)

    log.info("payload:", payload:toHex(), "长度:", #payload)
    log.info("计算CRC:", string.format("%04X", expected_crc))

    if received_crc ~= string.pack("<I2", expected_crc) then
        log.warn("CRC校验失败，主站发送过来的CRC：", received_crc:toHex(), "从站计算的CRC：",
            string.pack("<I2", expected_crc):toHex())
        return false
    end
    return true
end

--- 检查指定地址范围是否在合法寄存器区间内
-- @param start 起始地址（从1开始）
-- @param count 寄存器数量
-- @return boolean 如果地址范围有效返回 true，否则 false
function ModbusRTU_Slave:is_valid_register_range(start, count)
    if start < 0 or count < 1 then
        return false
    end

    -- 计算最大有效索引
    local max_index = self.holding_registers_count - 1
    -- 检查结束地址是否在有效范围内
    local end_address = start + count - 1

    log.info("保持寄存器地址范围：0 -", self.holding_registers_count - 1)

    if end_address > max_index then
        log.warn("寄存器地址超出范围", "开始地址=", start, "数量=", count, "结束地址=", end_address,
            "最大地址=", max_index)
        return false
    end
    return true
end

--- 检查指定地址范围是否在合法线圈区间内
-- @param start 起始地址（从1开始）
-- @param count 线圈数量
-- @return boolean 如果地址范围有效返回 true，否则 false
function ModbusRTU_Slave:is_valid_coil_range(start, count)
    if start < 0 or count < 1 then
        return false
    end

    -- 计算最大有效地址（线圈地址从1开始计数）
    local end_address = start + count - 1

    log.info("线圈地址范围：1 -", self.coils_count - 1)

    if end_address > self.coils_count then
        log.warn("线圈地址超出范围", "开始地址=", start, "数量=", count, "结束地址=", end_address,
            "最大地址=", self.coils_count - 1)
        return false
    end
    return true
end

--- 处理 Modbus 请求
-- @param frame 接收到的 Modbus RTU 帧数据
-- @return string|nil 返回响应帧，若出错则返回异常或 nil
function ModbusRTU_Slave:handle_modbus_request(frame)
    local addr = string.byte(frame, 1) -- 提取从机地址
    local fc = string.byte(frame, 2) -- 提取功能码
    log.info("原始帧:", string.format("%q", frame))

    log.info("开始处理 Modbus 请求，数据长度：", #frame, "，从站地址：", addr)
    log.debug("识别到功能码：", fc)

    -- 校验 CRC
    if not self:validate_crc(frame) then
        return self:generate_exception(fc, ExceptionCode.MemoryParityError) -- 异常码: MemoryParityError (0x08) - CRC16 校验失败
    end

    -- 地址不匹配
    if addr ~= self.config.slave_addr then
        log.warn("从站地址不匹配，目标地址：", addr, "，本机地址：", self.config.slave_addr)
        return nil
    end

    --- === 处理功能码 ===
    if fc == self.FunctionCode.ReadQ then -- 读取线圈 0x01
        log.warn("读取线圈状态0x01:")
        -- 请求帧格式：{1字节地址, 1字节功能码, 2字节起始地址, 2字节寄存器数量, 2字节 CRC 校验}
        if #frame < 8 then
            log.warn("请求数据太短，无法解析起始地址/数量")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        -- 解析起始地址和寄存器数量
        local start_addr, coil_count = string.unpack(">I2>I2", frame:sub(3, 6))
        log.info("请求读取线圈：起始地址=", start_addr, "，读取线圈的数量=", coil_count)

        -- 检验线圈数量（Modbus协议最大2000个）
        if (coil_count < 1 or coil_count > 2000) then
            log.warn("线圈数量超出范围，最大2000个")
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue) -- 异常码: IllegalDataValue (0x03) - 值超出设备范围
        end

        -- 检验地址范围
        if not self:is_valid_coil_range(start_addr, coil_count) then
            log.warn("线圈地址超出范围")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        -- 构建响应数据（每个字节表示8个线圈状态）
        local byte_count = math.ceil(coil_count / 8)
        local response_data = {}
        -- 初始化响应数据数组
        for i = 1, byte_count do
            response_data[i] = 0
        end

        -- 填充线圈状态数据
        for i = 0, coil_count - 1 do
            local coil_addr = i + start_addr
            local byte_idx = math.floor(i / 8) + 1 -- 获取字节索引（从1开始）
            local bit_idx = i % 8 -- 获取位索引（0-7）

            -- 线圈状态为 true 时设置对应位为 1
            if self.coils[coil_addr] then
                response_data[byte_idx] = response_data[byte_idx] | (1 << bit_idx)
            end
        end

        -- 构建响应帧：[地址][功能码][字节数][线圈状态数据][CRC]
        local response = string.char(addr, fc, byte_count)
        for i = 1, byte_count do
            response = response .. string.char(response_data[i])
        end

        -- 计算CRC并返回完整响应帧
        local crc = self:crc16(response)
        return response .. string.pack("<I2", crc)

    elseif fc == self.FunctionCode.ReadVW then -- 读取保持寄存器 0x03
        log.info("读保持寄存器")
        -- 请求帧最小长度应为 8 字节：1字节地址 + 1字节功能码 + 字节起始地址 + 2字节寄存器数量 + 2字节 CRC 校验
        if #frame < 8 then
            log.warn("请求数据太短，无法解析起始地址/数量")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        -- 解析 Modbus 请求中的“起始地址”和“寄存器数量”
        local start, count = string.unpack(">I2", frame:sub(3, 4)), string.unpack(">I2", frame:sub(5, 6))
        log.info("请求读取寄存器：起始地址=", start, "，读取寄存器的数量=", count)

        -- 判断请求的寄存器数量是否合法
        -- Modbus 协议规定最多一次读取 125 个寄存器（每个寄存器占 2 字节，总响应不超过 256 字节）
        if count > 125 then
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue) -- 异常码: IllegalDataValue (0x03) - 数据值无效或不可接受
        end

        -- 检查请求的地址加数量是否在合法范围内
        if not self:is_valid_register_range(start, count) then
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        local response = string.char(addr, fc) -- 构建响应帧头部：从站地址 + 功能码
        local byte_count = count * 2 -- 计算后续数据的字节数 = 寄存器数 * 2（每个寄存器占 2 字节）
        response = response .. string.char(byte_count) -- 添加字节数字段到响应帧中

        -- 循环读取每一个指定的寄存器值，并打包成大端格式写入响应帧
        for i = 0, count - 1 do
            local reg_value = self.holding_registers[start + i]
            -- 确保寄存器的值是数字类型，否则返回异常
            if type(reg_value) ~= "number" then
                log.warn("寄存器值不是数字类型:", tostring(reg_value))
                return self:generate_exception(fc, ExceptionCode.IllegalDataValue) -- 异常码: IllegalDataValue (0x03) - 数据值无效或不可接受
            end
            -- 将寄存器值以大端方式打包并追加到响应帧中
            response = response .. string.pack(">I2", reg_value)
        end

        -- 计算 CRC 校验码
        local crc = ModbusRTU_Slave:crc16(response)

        -- 追加 CRC 校验码（小端格式）到响应帧末尾
        return response .. string.pack("<I2", crc)
    elseif fc == self.FunctionCode.WriteQ then -- 写入单个线圈 0x05
        log.warn("写入单个线圈0x05:")
        -- 请求帧格式：1字节地址+1字节功能码+2字节线圈地址+2字节线圈值+2字节CRC校验码
        -- 值为0xFF00表示1，0x0000表示0
        if #frame < 8 then
            log.warn("写入单个线圈0x05:帧长度不足")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress)
        end

        -- 解析线圈地址和值
        local coil_addr, value = string.unpack(">I2I2", frame:sub(3, 6))
        log.info("写入单个线圈0x05:", coil_addr, "值=", value)

        -- 检验地址范围
        if coil_addr < 0 or coil_addr >= self.coils_count then
            log.warn("写入单个线圈0x05:地址超出范围")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress)
        end

        -- 检验值是否合法（只能是0xFF00 或 0x0000）
        if value ~= 0xFF00 and value ~= 0x0000 then
            log.warn("写入单个线圈值不合法（必须是0xFF00 或 0x0000）")
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue)
        end

        -- 更新线圈状态
        self.coils[coil_addr] = (value == 0xFF00) -- true 表示1；false 表示0
        log.info("线圈(Modbus地址)", coil_addr, "已设置为:", self.coils[coil_addr])

        -- 响应帧回显请求的地址，功能码，线圈地址和值
        local response = string.char(addr, fc) .. frame:sub(3, 6) -- 复用请求中的地址和值字段
        local crc = self:crc16(response)
        return response .. string.pack("<I2", crc)
    elseif fc == self.FunctionCode.WriteVW then -- 写入单个保持寄存器 0x06
        log.info("写入单个保持寄存器")

        if #frame < 8 then
            log.warn("帧长度不足")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        local start, value = string.unpack(">I2", frame:sub(3, 4)), string.unpack(">I2", frame:sub(5, 6))
        log.info("写入起始地址:", start, "写入寄存器的值:", value)

        -- 检查请求的地址加数量是否在合法范围内
        if not self:is_valid_register_range(start, 1) then
            log.warn("地址超出范围")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        -- 写入单个保持寄存器
        self.holding_registers[start] = value
        log.info("modbus写入单个保持寄存器的数据 value:", value)

        -- 构建响应帧
        local response = string.char(addr, fc) .. string.pack(">I2>I2", start, value)

        -- 计算 CRC 校验
        local crc = ModbusRTU_Slave:crc16(response)

        return response .. string.pack("<I2", crc)
    elseif fc == self.FunctionCode.WriteMQ then -- 写入多个线圈 0x0F 
        log.warn("写入多个线圈0x0F:", fc)

        -- 请求帧格式：1字节地址+1字节功能码+2字节起始地址+2字节数量+1字节技术+n字节数据+2字节CRC
        if #frame < 9 then
            log.warn("写入多个线圈0x0F:", "数据长度不足")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress)
        end

        -- 解析参数
        local start_addr, coil_count = string.unpack(">I2>I2", frame:sub(3, 6))
        local byte_count = string.byte(frame, 7)
        local data_start = 8 -- 数据起始位置
        local data_end = data_start + byte_count - 1

        log.info("请求写入多个线圈：起始地址=", start_addr, ",数量=", coil_count, "字节数量",
            byte_count)

        -- 校验参数合法性
        if coil_count < 1 or coil_count > 1968 then
            log.warn("参数错误：线圈数量超出范围（1~1968）")
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue)
        end
        if byte_count ~= math.ceil(coil_count / 8) then
            log.warn("参数错误：字节数量与线圈数量不符")
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue)
        end

        if not self:is_valid_coil_range(start_addr, coil_count) then
            log.warn("参数错误：线圈地址超出范围")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress)
        end

        -- 解析数据并更新线圈状态
        local data = frame:sub(data_start, data_end)
        for i = 0, coil_count - 1 do
            local coil_addr = start_addr + i
            local byte_idx = math.floor(i / 8) + 1 -- 数组字节索引
            local bit_idx = i % 8 -- 数组位索引

            -- 提取对应的值（1为ON,0为OFF）
            local byte_val = string.byte(data, byte_idx) or 0
            self.coils[coil_addr] = (byte_val & (1 << bit_idx)) ~= 0
            log.debug("线圈(Modbus)地址", coil_addr, "状态更新为:", self.coils[coil_addr])
        end

        -- 响应帧：地址、功能码、起始地址、线圈数量、CRC
        local response = string.pack(">BB", addr, fc)
        response = response .. string.pack(">I2>I2", start_addr, coil_count)
        local crc = self:crc16(response)
        return response .. string.pack("<I2", crc)
    elseif fc == self.FunctionCode.WriteMVW then -- 写入多个保持寄存器 0x10
        log.info("写入多个保持寄存器")

        -- 请求帧最小长度应为 9 字节：1字节地址 + 1字节功能码 +2字节起始地址 +2字节寄存器数量 +1字节数据字节数 +至少 2字节数据 +2字节 CRC 校验
        if #frame < 9 then
            log.warn("帧长度不足")
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress) -- 异常码: IllegalDataAddress (0x02) - 地址无效或超出设备范围
        end

        local start, count = string.unpack(">I2", frame:sub(3, 4)), string.unpack(">I2", frame:sub(5, 6))
        local byte_count = string.byte(frame, 7) -- 数据字段的字节计数
        log.info("写入寄存器的起始地址：", start, " 写入寄存器的数量：", count,
            " 数据字节数：", byte_count)

        if not self:is_valid_register_range(start, count) then
            log.warn("写入寄存器的范围超出范围", "期望=", count * 2, "，实际=", count)
            return self:generate_exception(fc, ExceptionCode.IllegalDataAddress)
        end

        if byte_count ~= count * 2 then
            log.warn("数据字节数与寄存器数量不匹配")
            return self:generate_exception(fc, ExceptionCode.IllegalDataValue)
        end

        local data_start = 8 -- 第8个字节开始是数据部分
        for i = 1, count do
            local offset = data_start + (i - 1) * 2
            local value = string.unpack(">I2", frame:sub(offset, offset + 1))
            self.holding_registers[start + i - 1] = value
            log.info("modbus写入多个保持寄存器第", i, "个的的数据是 value:", value)
        end

        -- 构造响应帧
        local response = string.char(addr, fc) .. string.pack(">I2", start) .. string.pack(">I2", count)
        -- local response = string.pack(">B B I2 I2", addr, fc, start, count)
        -- 计算 CRC 校验
        local crc = ModbusRTU_Slave:crc16(response)

        log.info("构造的响应帧（不含CRC）:", response:toHex())
        log.info("计算的CRC:", string.format("%04X", crc))

        return response .. string.pack("<I2", crc)

    else
        log.warn("未实现的功能码:", fc)
        return self:generate_exception(fc, ExceptionCode.IllegalFunction)
    end
end

return ModbusRTU_Slave
